package net.deuce.perf;

import net.deuce.rc.client.*;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.BeansException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.dom4j.DocumentException;

import java.io.*;
import java.net.URL;
import java.text.NumberFormat;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

public class Driver implements InitializingBean, BeanFactoryAware {

    private Configuration configuration;
    private String testSummary;
    private String jvmMonitorLogSeparator = ",";
    private String vmstatLogSeparator = " ";
    private Pattern numberPattern = Pattern.compile("^[0-9]+$");
    private BeanFactory beanFactory;
    private boolean manualMode = false;

    private Log log = LogFactory.getLog(getClass());

    public Driver() {
        //log.trace("");

    }

    public Driver(Configuration configuration) {
        //log.trace("");
        this.configuration = configuration;
    }

    public Configuration getConfiguration() {
        //log.trace("");
        return configuration;
    }

    public void setConfiguration(Configuration configuration) {
        //log.trace("");
        this.configuration = configuration;
    }

    public String getVmstatLogSeparator() {
        //log.trace("");
        return vmstatLogSeparator;
    }

    public void setVmstatLogSeparator(String vmstatLogSeparator) {
        //log.trace("");
        this.vmstatLogSeparator = vmstatLogSeparator;
    }

    public void execute(File resultsDir) throws IOException, RemoteException {
        //log.trace("");
        while (testSummary == null || testSummary.trim().length() == 0) {
            System.out.print("Enter test summary: ");
            testSummary = new BufferedReader(new InputStreamReader(System.in)).readLine().trim();
        }
        fetchResults(resultsDir);
    }

    public void execute(boolean manualMode) throws IOException, InterruptedException, RemoteException {

        this.manualMode = manualMode;
        //log.trace("");
        //testSummary = "skdjfksdf";
        while (testSummary == null || testSummary.trim().length() == 0) {
            System.out.print("Enter test summary: ");
            testSummary = new BufferedReader(new InputStreamReader(System.in)).readLine().trim();
        }

        if (manualMode) {
            System.out.print("Please start all servers and test clients.");
            System.out.print("When tests are finished, place all log and results in this directory: " + configuration.getResultsDir().getAbsolutePath());
            System.out.print("\nHit enter when ready...");
            new BufferedReader(new InputStreamReader(System.in)).readLine();
            fetchResults(configuration.getResultsDir());
        } else {
            startServers();
            startClients();
            fetchResults(null);
        }
    }

    private String getResource(String name) throws IOException {
        //log.trace("");
        InputStream is = null;

        try {
            is = getClass().getResourceAsStream(name);
            return getFile(is);
        } finally {
            if (is != null) {
                is.close();
                is = null;
            }
        }
    }

    private String getFile(File f) throws IOException {
        //log.trace("");
        InputStream is = null;

        try {
            is = new FileInputStream(f);
            return getFile(is);
        } finally {
            if (is != null) {
                is.close();
                is = null;
            }
        }
    }

    private String getFile(InputStream is) throws IOException {
        //log.trace("");
        byte[] buf = new byte[1024];
        int numRead;
        StringBuffer result = new StringBuffer();

        while ((numRead = is.read(buf, 0, 1024)) >= 0) {
            result.append(new String(buf, 0, numRead));
        }
        return result.toString();
    }

    private void saveFile(File f, String contents) throws IOException {
        //log.trace("");
        PrintWriter out = null;
        try {
            out = new PrintWriter(new FileWriter(f));
            out.print(contents);
        } finally {
            if (out != null) {
                out.close();
                out = null;
            }
        }
    }

    private void saveFile(File f, InputStream contents) throws IOException {
        //log.trace("");
        f.getParentFile().mkdirs();
        OutputStream os = null;
        try {
            byte[] buf = new byte[1024];
            int numRead;
            os = new FileOutputStream(f);
            while ((numRead = contents.read(buf, 0, 1024)) >= 0) {
                os.write(buf, 0, numRead);
            }
        } finally {
            if (os != null) {
                os.close();
                os = null;
            }
        }
    }
    
    private String trimWhitespace(String s) {
        //log.trace("");
        while (s.contains("  ")) {
            s = s.replaceAll("  ", " ");
        }
        return s;
    }

    private void replaceToken(StringBuffer sb, String source, String target) {
        //log.trace("");
        int i;
        while ((i = sb.indexOf(source)) >= 0) {
            sb.replace(i, i + source.length(), target);
        }
    }

    private int getValue(String[] split, int field) {
        //log.trace("");
        if (field >= split.length) return 0;
        Matcher m = numberPattern.matcher(split[field]);
        if (m.find()) {
            return new Integer(split[field]);
        }
        return 0;
    }

    private void incrementUserCount(Map<Long, Integer> m, long t, int userCount) {
        //log.trace("");
        Integer count = m.get(t);
        if (count == null) {
            count = new Integer(userCount);
        } else {
            count = count + userCount;
        }
        m.put(t, count);
    }

    private int getMaxUserCount(Map<Long, Integer> m) {
        //log.trace("");
        int maxValue = 0;

        for (Integer i : m.values()) {
            if (i > maxValue) {
                maxValue = i;
            }
        }
        return maxValue;
    }

    private void fetchResults(File previousResultsDir) throws IOException, RemoteException {
        //log.trace("");
        String title = configuration.getTimestamp() + " - " + testSummary;

        StringBuffer drawChartFunctions = new StringBuffer();
        StringBuffer drawChartFunctionExecution = new StringBuffer();
        StringBuffer charts = new StringBuffer();
        int serverCount = 0;
        long elapsedTime = 0;
        Map<Long, Integer> userCountOverTime = new HashMap<Long, Integer>();

        File resultsDir = previousResultsDir;
        if (resultsDir == null) {
            resultsDir = configuration.getResultsDir();
        }

        /*
        for(String client : configuration.getClient().getRemoteHosts()) {
            if (!debug) {
                RemoteCommandClient getFileCmd = new RemoteCommandClient(RemoteCommandType.GetFile,
                        server.getName(), server.getRemoteCommandPort(), null);
                getFileCmd.setFile(serverLog);
                getFileCmd.setLocalFilename(split[split.length-1]);
                getFileCmd.setResultsDir(configuration.getResultsDir().getAbsolutePath());
                getFileCmd.execute();
            }
        }
        */

        if (previousResultsDir == null) {
            for (AppServerConfiguration server : configuration.getAppServers()) {
                for (RemoteBackgroundCommand logCmd : server.getLogs()) {
                    File localFile = new File(resultsDir, server.getName() + "-" + logCmd.getLabel() + ".log");
                    StopBackgroundCommand stopLogCmd = (StopBackgroundCommand) beanFactory.getBean("stopBackgroundPrototype", StopBackgroundCommand.class);
                    stopLogCmd.setRemoteCommandClient(logCmd.getRemoteCommandClient());
                    stopLogCmd.setLocalFile(localFile);
                    stopLogCmd.setRemoteId(logCmd.getRemoteId());
                    stopLogCmd.execute();
                }

                for (String serverLog : configuration.getAppServerLogs()) {
                    String[] split = serverLog.split("/");
                    FileTransferRemoteCommand getFileCmd = (FileTransferRemoteCommand) beanFactory.getBean("getFilePrototype", FileTransferRemoteCommand.class);
                    getFileCmd.setRemoteCommandClient(server.getRemoteCommandClient());
                    getFileCmd.setRemoteFile(serverLog);
                    getFileCmd.setLocalFile(new File(resultsDir, split[split.length - 1]));
                    getFileCmd.execute();
                }
            }

            for (AppServerConfiguration server : configuration.getDbServers()) {
                for (RemoteBackgroundCommand logCmd : server.getLogs()) {
                    File localFile = new File(resultsDir, server.getName() + "-" + logCmd.getLabel() + ".db.log");
                    StopBackgroundCommand stopLogCmd = (StopBackgroundCommand) beanFactory.getBean("stopBackgroundPrototype", StopBackgroundCommand.class);
                    stopLogCmd.setRemoteCommandClient(logCmd.getRemoteCommandClient());
                    stopLogCmd.setLocalFile(localFile);
                    stopLogCmd.setRemoteId(logCmd.getRemoteId());
                    stopLogCmd.execute();
                }
            }
        }

        for (AppServerConfiguration server : configuration.getAppServers()) {
            serverCount++;

            StringBuffer usedMem = new StringBuffer();
            StringBuffer guestUsers = new StringBuffer();
            StringBuffer authUsers = new StringBuffer();
            StringBuffer gcTime = new StringBuffer();
            StringBuffer cpuUtilization = new StringBuffer();
            long maxValue = 0;


            File jvmMonLog = new File(resultsDir, server.getName() + "-monitoring.log");
            LineNumberReader reader = null;
            try  {
                reader = new LineNumberReader(new FileReader(jvmMonLog));
            } catch (FileNotFoundException fnfe) {
                System.err.println("Could not find monitoring log: " + jvmMonLog.getAbsolutePath());
                System.err.println("Fetch the log from " + server.getName() + ", place it in " +
                    resultsDir.getAbsolutePath() + " as " + server.getName() + "-monitoring.log and rerun with -r option.");
                System.exit(-1);
            }
            String line;
            while ((line = reader.readLine()) != null) {
                String[] split = line.split(jvmMonitorLogSeparator);
                if (reader.getLineNumber() > 1) {
                    elapsedTime = new Long(split[0]);
                    int usedMemValue = getValue(split, 2) / 1024000;
                    if (usedMem.length() > 0) usedMem.append(',');
                    usedMem.append('"').append(usedMemValue).append('"');

                    int guestUsersValue = getValue(split, 3);
                    if (guestUsers.length() > 0) guestUsers.append(',');
                    guestUsers.append('"').append(guestUsersValue).append('"');

                    int authUsersValue = getValue(split, 4)*10;
                    if (authUsers.length() > 0) authUsers.append(',');
                    authUsers.append('"').append(authUsersValue).append('"');
                    incrementUserCount(userCountOverTime, elapsedTime, getValue(split, 4));

                    long gcTimeValue = getValue(split, 5);
                    if (gcTime.length() > 0) gcTime.append(',');
                    gcTime.append('"').append(gcTimeValue).append('"');

                    if (usedMemValue > maxValue) maxValue = usedMemValue;
                    if (guestUsersValue > maxValue) maxValue = guestUsersValue;
                    if (authUsersValue > maxValue) maxValue = authUsersValue;
                    if (gcTimeValue > maxValue) maxValue = gcTimeValue;
                }
            }
            elapsedTime /= 60;
            reader.close();

            File vmstatLog = new File(resultsDir, server.getName() + "-vmstat.log");
            try  {
                reader = new LineNumberReader(new FileReader(vmstatLog));
            } catch (FileNotFoundException fnfe) {
                System.err.println("Could not find monitoring log: " + vmstatLog.getAbsolutePath());
                System.err.println("Fetch the log from " + server.getName() + ", place it in " +
                        resultsDir.getAbsolutePath() + " as " + server.getName() + "-vmstat.log and rerun with -r option.");
                System.exit(-1);
            }
            reader = new LineNumberReader(new FileReader(vmstatLog));
            while ((line = reader.readLine()) != null) {
                line = trimWhitespace(line);
                String[] split = line.split(vmstatLogSeparator);
                if (split.length >= 18) {
                    Matcher m = numberPattern.matcher(split[15]);
                    if (m.find()) {
                        int cpuValue = 100 - new Integer(split[15]);
                        if (cpuUtilization.length() > 0) cpuUtilization.append(',');
                        cpuUtilization.append('"').append(cpuValue).append('"');
                        if (cpuValue > maxValue) maxValue = cpuValue;
                    }
                }
            }
            reader.close();

            try {
                drawChartFunctions.append("function drawChart").append(serverCount).append("() {\n");
                drawChartFunctions.append(getResource("drawChartTemplate.js"));
                drawChartFunctions.append('\n');
                drawChartFunctions.append("}\n");
                replaceToken(drawChartFunctions, "%serverCount%", new Integer(serverCount).toString());
                replaceToken(drawChartFunctions, "%maxValue%", new Long((long) Math.ceil((double)maxValue / 100.0) * 100).toString());
                replaceToken(drawChartFunctions, "%usedMem%", usedMem.toString());
                replaceToken(drawChartFunctions, "%guestUsers%", guestUsers.toString());
                replaceToken(drawChartFunctions, "%authUsers%", authUsers.toString());
                replaceToken(drawChartFunctions, "%gcTime%", gcTime.toString());
                replaceToken(drawChartFunctions, "%cpuUtilization%", cpuUtilization.toString());

                drawChartFunctionExecution.append("drawChart").append(serverCount).append("()\n");

                charts.append(getResource("chartAreaTemplate.html"));
                replaceToken(charts, "%serverCount%", new Integer(serverCount).toString());
                replaceToken(charts, "%appServer%", server.getName());
            } catch (Exception e) {
                e.printStackTrace();
                throw new RuntimeException(e);
            }

        }

        for (AppServerConfiguration server : configuration.getDbServers()) {
            serverCount++;
            long maxValue = 0;
            StringBuffer cpuUtilization = new StringBuffer();
            File vmstatLog = new File(resultsDir, server.getName() + "-vmstat.db.log");
            LineNumberReader reader = new LineNumberReader(new FileReader(vmstatLog));
            String line;
            while ((line = reader.readLine()) != null) {
                line = trimWhitespace(line);
                String[] split = line.split(vmstatLogSeparator);
                if (split.length >= 23) {
                    Matcher m = numberPattern.matcher(split[22]);
                    if (m.find()) {
                        int cpuValue = 100 - new Integer(split[22]);
                        if (cpuUtilization.length() > 0) cpuUtilization.append(',');
                        cpuUtilization.append('"').append(cpuValue).append('"');
                        if (cpuValue > maxValue) maxValue = cpuValue;
                    }
                }
            }
            reader.close();

            try {
                drawChartFunctions.append("function drawChart").append(serverCount).append("() {\n");
                drawChartFunctions.append(getResource("drawDbChartTemplate.js"));
                drawChartFunctions.append('\n');
                drawChartFunctions.append("}\n");
                replaceToken(drawChartFunctions, "%serverCount%", new Integer(serverCount).toString());
                replaceToken(drawChartFunctions, "%maxValue%", new Long((long) Math.ceil((double)maxValue / 100.0) * 100).toString());
                replaceToken(drawChartFunctions, "%cpuUtilization%", cpuUtilization.toString());

                drawChartFunctionExecution.append("drawChart").append(serverCount).append("()\n");

                charts.append(getResource("chartAreaTemplate.html"));
                replaceToken(charts, "%serverCount%", new Integer(serverCount).toString());
                replaceToken(charts, "%appServer%", server.getName());
            } catch (Exception e) {
                e.printStackTrace();
                throw new RuntimeException(e);
            }
        }

        Double[] thresholds = {new Double(5000.0), new Double(7000.0), new Double(10000.0)};
        ClientStats clientStats = null;
        try {
            clientStats = new JMeterSummaryStats(new File(resultsDir, "samples.log"),
                Arrays.asList(thresholds)).calculateStats();
        } catch (DocumentException de) {
            System.err.println("samples.log was invalid possibly from an interrupted jmeter process.");
            System.err.println("Correct it and rerun with -r option.");
            System.exit(-1);
        }
        try {
            File breakdown = new File(resultsDir, "breakdown.txt");
            StringBuffer bsb = new StringBuffer();
            for (SimpleStats ss : clientStats.getBreakdown()) {
                bsb.append("Label: " + ss.getName()).append('\n');
                bsb.append('\t').append("Min: " + ss.getMinTime()/1000).append('\n');
                bsb.append('\t').append("Max: " + ss.getMaxTime()/1000).append('\n');
                bsb.append('\t').append("Avg: " + ss.getAvgTime()/1000).append('\n');
            }

            saveFile(breakdown, bsb.toString());

            NumberFormat decimalFormat = NumberFormat.getNumberInstance();
            decimalFormat.setMaximumFractionDigits(2);
            decimalFormat.setMinimumFractionDigits(2);
            NumberFormat percentFormat = NumberFormat.getPercentInstance();
            percentFormat.setMaximumFractionDigits(2);
            percentFormat.setMinimumFractionDigits(2);
            File result = new File(resultsDir, "result.html");
            StringBuffer resultHtml = new StringBuffer(getResource("resultTemplate.html"));
            replaceToken(resultHtml, "%drawChartFunctions%", drawChartFunctions.toString());
            replaceToken(resultHtml, "%drawChartFunctionExecutions%", drawChartFunctionExecution.toString());
            replaceToken(resultHtml, "%charts%", charts.toString());
            replaceToken(resultHtml, "%title%", title);
            replaceToken(resultHtml, "%maxConcurrentUsers%", new Integer(getMaxUserCount(userCountOverTime)).toString());
            replaceToken(resultHtml, "%elapsedTime%", new Long(elapsedTime).toString());
            replaceToken(resultHtml, "%avgResponseTime%", decimalFormat.format(clientStats.getAvgResponseTime()/1000));
            replaceToken(resultHtml, "%minResponseTime%", new Long(clientStats.getMinTime()/1000).toString());
            replaceToken(resultHtml, "%maxResponseTime%", new Long(clientStats.getMaxTime()/1000).toString());
            replaceToken(resultHtml, "%errorRate%", percentFormat.format(clientStats.getErrorRate() * 100));
            replaceToken(resultHtml, "%percentResponses5%", percentFormat.format(clientStats.getThresholdResult(new Double(5000.0))));
            replaceToken(resultHtml, "%percentResponses7%", percentFormat.format(clientStats.getThresholdResult(new Double(7000.0))));
            replaceToken(resultHtml, "%percentResponses10%", percentFormat.format(clientStats.getThresholdResult(new Double(10000.0))));

            saveFile(result, resultHtml.toString());

            File indexFile = new File(configuration.getBaseResultsDir(), "index.html");
            String indexTemplate = null;
            if (indexFile.exists()) {
                indexTemplate = getFile(indexFile);
            } else {
                indexTemplate = getResource("index.html");
            }
            File jqchart = new File(configuration.getBaseResultsDir(), "jqchart");
            if (!jqchart.exists()) {
                URL url = getClass().getResource("jqchart.zip");
                int pos = url.getPath().indexOf('!');
                String jarFile = url.getPath().substring(0, pos);
                if (jarFile.startsWith("jar:")) {
                    jarFile = jarFile.substring(4);
                }
                if (jarFile.startsWith("file:")) {
                    jarFile = jarFile.substring(5);
                }
                File f = new File(jarFile);
                ZipFile jar = new ZipFile(f);
                ZipEntry entry = jar.getEntry("net/deuce/perf/jqchart.zip");
                File tmpFile = new File(System.getProperty("java.io.tmpdir"), "jqchart.zip");
                saveFile(tmpFile, jar.getInputStream(entry));

                jar = new ZipFile(tmpFile);
                Enumeration<ZipEntry> en = (Enumeration<ZipEntry>) jar.entries();
                while (en.hasMoreElements()) {
                    entry = en.nextElement();
                    if (!entry.getName().endsWith("/")) {
                        f = new File(configuration.getBaseResultsDir(), entry.getName());
                        saveFile(f, jar.getInputStream(entry));
                    }
                }
                tmpFile.delete();
            }

            saveFile(indexFile, indexTemplate.replaceAll("<!-- TEST-RESULT-STUB -->",
                    "<li><a target='_blank' href='" + result.toURL() + "'>" + title + "</a></li><!-- TEST-RESULT-STUB -->"
            ));
            System.out.println("Results: " + result.getPath());
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }

    private void startServers() {
        //log.trace("");
        List<StartServerWorker> workers = new LinkedList<StartServerWorker>();

        for (AppServerConfiguration appServer : configuration.getAppServers()) {
            System.out.println("Starting thread for app server: " + appServer.getName());
            StartServerWorker worker = new StartServerWorker(appServer);
            workers.add(worker);
            new Thread(worker).start();
        }
        for (AppServerConfiguration dbServer : configuration.getDbServers()) {
            System.out.println("Starting thread for app server: " + dbServer.getName());
            StartServerWorker worker = new StartServerWorker(dbServer);
            workers.add(worker);
            new Thread(worker).start();
        }
        boolean running = true;
        while (running) {
            running = false;
            for (StartServerWorker worker : workers) {
                if (worker.isRunning()) {
                    running = true;
                }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
        for (StartServerWorker worker : workers) {
            if (!worker.isSuccessful()) {
                throw new RuntimeException("starting server failed: " + worker.getAppServer().getName());
            }
        }
        System.out.println("App servers started");
    }

    private void startClients() throws IOException, InterruptedException {
        //log.trace("");
        String jmeter = "jmeter";

        ClientConfiguration clientConfig = configuration.getClient();

        List<String> commandArgs = new LinkedList<String>();
        commandArgs.add(clientConfig.getExecutable().getAbsolutePath());
        commandArgs.add("-n");

        StringBuffer hosts = new StringBuffer();
        if (clientConfig.getRemoteHosts().size() > 0) {
            commandArgs.add("-R");
            for (int i = 0; i < clientConfig.getRemoteHosts().size(); i++) {
                if (i > 0) hosts.append(',');
                hosts.append(clientConfig.getRemoteHosts().get(i));
            }
            commandArgs.add(hosts.toString());
        }

        Map<String, String> m = clientConfig.getProperties();
        for (String name : m.keySet()) {
            commandArgs.add("-J");
            commandArgs.add(name + '=' + m.get(name));
        }

        commandArgs.add("-t");
        commandArgs.add(clientConfig.getTestPlan().getAbsolutePath());
        commandArgs.add("-j");
        commandArgs.add(new File(configuration.getResultsDir().getAbsolutePath(), "jmeter.log").getAbsolutePath());
        commandArgs.add("-l");
        commandArgs.add(new File(configuration.getResultsDir().getAbsolutePath(), "samples.log").getAbsolutePath());

        StringBuffer cmd = new StringBuffer();
        for (String s : commandArgs) {
            if (cmd.length() > 0) cmd.append(' ');
            cmd.append(s);
        }
        System.out.println("Executing " + cmd.toString());
        Process p = new ProcessBuilder(commandArgs).start();
        int rc = p.waitFor();
        switch (rc) {
            case 1:
                throw new RuntimeException("jmeter failed. Is the jmeter server running on all client hosts: " +hosts.toString());
            case 0: // kill -9
            case 137: // kill -9
                break;
            default:
                throw new RuntimeException("jmeter failed rc="+rc);
        }
    }

    public void afterPropertiesSet() throws Exception {
    }

    public String getJvmMonitorLogSeparator() {
        //log.trace("");
        return jvmMonitorLogSeparator;
    }

    public void setJvmMonitorLogSeparator(String jvmMonitorLogSeparator) {
        //log.trace("");
        this.jvmMonitorLogSeparator = jvmMonitorLogSeparator;
    }

    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        //log.trace("");
        this.beanFactory = beanFactory;
    }

    private static class StartServerWorker implements Runnable {
        private AppServerConfiguration appServer;
        private boolean running = true;
        Log log = LogFactory.getLog(getClass());
        private boolean successful = false;

        public StartServerWorker(AppServerConfiguration appServer) {
            //log.trace("");
            this.appServer = appServer;
        }

        public AppServerConfiguration getAppServer() {
            return appServer;
        }

        public void run() {
            //log.trace("");
            try {
                for (RunCommandWrapper rcw : appServer.getStartCommands()) {
                    System.out.println("Executing " + rcw.getLabel() + " on " +
                        appServer.getRemoteCommandClient().getServer() + ":" +
                        appServer.getRemoteCommandClient().getPort());
                    try {
                        rcw.execute();
                    } catch (RemoteException re) {
                        if (rcw.isHaltOnError()) throw re;
                    }
                }
                for (RemoteCommand rc : appServer.getLogs()) {
                    System.out.println("Starting log " + rc.getLabel() + " on " +
                        appServer.getRemoteCommandClient().getServer() + ":" +
                        appServer.getRemoteCommandClient().getPort());
                    rc.execute();
                }
                successful = true;
            } catch (RemoteException e) {
                e.printStackTrace();
            } finally {
                running = false;
            }
        }

        public boolean isRunning() {
            //log.trace("");
            return running;
        }

        public boolean isSuccessful() {
            return successful;
        }

        public void setSuccessful(boolean successful) {
            this.successful = successful;
        }
    }
}
